package weka.classifiers.meta;

import java.io.Serializable;
import java.util.Enumeration;
import java.util.Vector;

import weka.classifiers.AbstractClassifier;
import weka.classifiers.Classifier;
import weka.core.AdditionalMeasureProducer;
import weka.core.Attribute;
import weka.core.Capabilities;
import weka.core.CapabilitiesHandler;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.RevisionUtils;
import weka.core.Utils;
import weka.core.Capabilities.Capability;

public class RuleExtract extends AbstractClassifier implements
        OptionHandler,
        Serializable, 
        AdditionalMeasureProducer,
        CapabilitiesHandler {

    /** Variable for storing the generated base classifier. */
    private Classifier baseClassifier;
    /** Variable for storing the generated rule extraction classifier. */
    private Classifier ruleExtractionClassifier;
    private boolean addProbabilities = false;
    private Instances savedData;

    public RuleExtract() throws Exception {
        baseClassifier = defaultBaseClassifier();
        ruleExtractionClassifier = defaultRuleExtractionClassifier();
    }

    /**
     * String describing default classifier.
     */
    protected String defaultRuleExtractionClassifierString() {

        return "weka.classifiers.trees.Grex";
    }

    public String defaultBaseClassifierString() {
        return "weka.classifiers.trees.RandomForest";
    }

    /**
     * String describing default classifier.
     */
    protected Classifier defaultRuleExtractionClassifier() throws Exception {

        return new weka.classifiers.trees.Grex();
    }

    public Classifier defaultBaseClassifier() {
        return new weka.classifiers.trees.RandomForest();
    }

    /**
     * Returns a string describing classifier
     * @return a description suitable for
     * displaying in the explorer/experimenter gui
     */
    public String globalInfo() {

        return "Class for extracting rules from one (opaque) classifier with another" + "(transitive) classifier.";
    }

    /**
     * Returns the revision string.
     *
     * @return		the revision
     */
    public String getRevision() {
        return RevisionUtils.extract("$Revision: 1.1 $");
    }

    /**
     * Generates a classifier.
     *
     * @param data set of instances serving as training data
     * @throws Exception if the classifier has not been generated
     *   successfully
     * @todo Implement this weka.classifiers.Classifier method
     */
    public void buildClassifier(Instances data) throws Exception {
        Instances newData = new Instances(data);
        baseClassifier.buildClassifier(newData);

        for (int i = 0; i < newData.numInstances(); i++) {
            double classification = baseClassifier.classifyInstance(newData.instance(i));
            int numC = newData.numClasses();
            if (classification != newData.instance(i).classValue()) {
                newData.instance(i).setClassValue(classification);
            }
            assert (numC == newData.numClasses());
        }
        if (getAddProbabilityColumns()) {
            int firstModel = newData.numAttributes() - 1, column = firstModel;
            for (int i = 0; i < newData.numClasses(); i++) {
                newData.insertAttributeAt(new Attribute("probability_" + i), column++);
            }
            for (int i = 0; i < data.numInstances(); i++) {
                column = firstModel;
                double[] pred = baseClassifier.distributionForInstance(data.instance(i));
                for (int n = 0; n < newData.numClasses(); n++) {
                    newData.instance(i).setValue(column++, pred[n]);
                }
            }
        }

        newData.deleteWithMissingClass();

        ruleExtractionClassifier.buildClassifier(newData);
        savedData = newData;
    }

    /**
     * Classifies a given instance using the selected classifier.
     *
     * @param instance the instance to be classified
     * @exception Exception if instance could not be classified
     * successfully
     */
    public double[] distributionForInstance(Instance instance) throws Exception {
        Instance inst = instance;
        if (getAddProbabilityColumns()) {
            int firstModel = instance.numAttributes() - 1, column = firstModel;

            inst = savedData.lastInstance();
            for (int i = 0; i < instance.numAttributes(); i++) {
                inst.setValue(i, instance.value(i));
            }
            column = firstModel;
            double[] pred = baseClassifier.distributionForInstance(inst);
            for (int n = 0; n < inst.numClasses(); n++) {
                inst.setValue(column++, pred[n]);
            }
        }
        return ruleExtractionClassifier.distributionForInstance(inst);
    }

    public Capabilities getCapabilities() {
        Capabilities result;

        if (baseClassifier != null) {
            result = baseClassifier.getCapabilities();
        } else {
            result = new Capabilities(this);
        }

        if (ruleExtractionClassifier != null) {
            result.and(ruleExtractionClassifier.getCapabilities());
        }

        // set dependencies
        for (Capability cap : Capability.values()) {
            result.enableDependency(cap);
        }

        result.setOwner(this);

        return result;
    }

    /**
     * Returns an enumeration describing the available options
     *
     * @return an enumeration of all the available options
     */
    public Enumeration listOptions() {

        Vector newVector = new Vector(3);

        newVector.addElement(new Option(
                "\tFull name of base classifier.\n" + "\t(default: " + defaultBaseClassifierString() + ")",
                "B", 1, "-B"));

        newVector.addElement(new Option(
                "",
                "", 0, "\nOptions specific to classifier " + baseClassifier.getClass().getName() + ":"));
        Enumeration enu = ((OptionHandler) baseClassifier).listOptions();
        while (enu.hasMoreElements()) {
            newVector.addElement(enu.nextElement());
        }

        newVector.addElement(new Option(
                "\tFull name of rule extraction classifier.\n" + "\t(default: " + defaultRuleExtractionClassifierString() + ")",
                "R", 1, "-R"));

        newVector.addElement(new Option(
                "",
                "", 0, "\nOptions specific to classifier " + ruleExtractionClassifier.getClass().getName() + ":"));
        enu = ((OptionHandler) ruleExtractionClassifier).listOptions();
        while (enu.hasMoreElements()) {
            newVector.addElement(enu.nextElement());
        }

        newVector.addElement(new Option(
                addProbabilityColumnsTipText(),
                "P", 0, "-P"));

        return newVector.elements();
    }

    /**
     * Parses a given list of options. Valid options are:<p>
     *
     * -B classifierstring <br>
     * Classifierstring should contain the full class name of a scheme
     * included for selection followed by options to the classifier
     * (required, option should be used once for each classifier).<p>
     *
     * @param options the list of options as an array of strings
     * @exception Exception if an option is not supported
     */
    public void setOptions(String[] options) throws Exception {
        String classifierString = Utils.getOption('B', options);
        String[] classifierSpec = Utils.splitOptions(classifierString);
        String classifierName;
        if (classifierSpec.length == 0) {
            classifierName = defaultBaseClassifierString();
        } else {
            classifierName = classifierSpec[0];
            classifierSpec[0] = "";
        }
        baseClassifier = AbstractClassifier.forName(classifierName, classifierSpec);

        /*String classifierName = Utils.getOption('B', options);

        if (classifierName.length() > 0) {

        // This is just to set the classifier in case the option
        // parsing fails.
        baseClassifier = Classifier.forName(classifierName, null);
        baseClassifier = Classifier.forName(classifierName,
        Utils.partitionOptions(options));
        } else {

        // This is just to set the classifier in case the option
        // parsing fails.
        baseClassifier = Classifier.forName(defaultClassifierString(), null);
        baseClassifier = Classifier.forName(defaultClassifierString(),
        Utils.partitionOptions(options));
        }*/
        classifierString = Utils.getOption('R', options);
        classifierSpec = Utils.splitOptions(classifierString);
        if (classifierSpec.length == 0) {
            classifierName = defaultRuleExtractionClassifierString();
        } else {
            classifierName = classifierSpec[0];
            classifierSpec[0] = "";
        }
        ruleExtractionClassifier = AbstractClassifier.forName(classifierName, classifierSpec);

        /*classifierName = Utils.getOption('R', options);

        if (classifierName.length() > 0) {

        // This is just to set the classifier in case the option
        // parsing fails.
        ruleExtractionClassifier = Classifier.forName(classifierName, null);
        ruleExtractionClassifier = Classifier.forName(classifierName,
        Utils.partitionOptions(options));
        } else {

        // This is just to set the classifier in case the option
        // parsing fails.
        ruleExtractionClassifier = Classifier.forName(defaultClassifierString(), null);
        ruleExtractionClassifier = Classifier.forName(defaultClassifierString(),
        Utils.partitionOptions(options));
        }*/

        if (Utils.getFlag('P', options)) {
            setAddProbabilityColumns(true);
        }
    }

    /**
     * Gets the current settings of the Classifier.
     *
     * @return an array of strings suitable for passing to setOptions
     */
    public String[] getOptions() {
        /*
        String[] baseOptions = ((OptionHandler) baseClassifier).getOptions();
        int baseOptionsLength = baseOptions.length;
        if (baseOptionsLength > 0) {
        baseOptionsLength++; // for the double hyphen
        }

        String[] ruleOptions = ((OptionHandler) ruleExtractionClassifier).getOptions();
        int ruleOptionsLength = ruleOptions.length;
        if (ruleOptionsLength > 0) {
        ruleOptionsLength++; // for the double hyphen
        }//*/

        int probabilityLength = addProbabilities ? 1 : 0;
        String[] options = new String[4 + probabilityLength];
        //baseOptionsLength + ruleOptionsLength + 4 + probabilityLength];

        int current = 0;
        options[current++] = "-B";
        options[current++] = baseClassifier.getClass().getName() + " "
                + Utils.joinOptions(((OptionHandler) baseClassifier).getOptions());
        /*options[current++] = baseClassifier.getClass().getName();

        if (baseOptions.length > 0) {
        options[current++] = "--";
        System.arraycopy(baseOptions, 0, options, current,
        baseOptions.length);
        current += baseOptions.length;
        }*/

        options[current++] = "-R";
        options[current++] = ruleExtractionClassifier.getClass().getName() + " "
                + Utils.joinOptions(((OptionHandler) ruleExtractionClassifier).getOptions());
        /*options[current++] = ruleExtractionClassifier.getClass().getName();

        if (ruleOptions.length > 0) {
        options[current++] = "--";
        System.arraycopy(ruleOptions, 0, options, current,
        ruleOptions.length);
        current += ruleOptions.length;
        }*/

        if (addProbabilities) {
            options[current++] = "-P";
        }

        return options;
    }

    /**
     * Returns the tip text for this property
     * @return tip text for this property suitable for
     * displaying in the explorer/experimenter gui
     */
    public String addProbabilityColumnsTipText() {
        return "Adds columns with probability distributions from base classifier for each class.";
    }

    /**
     * Get the value of cro.
     *
     * @return Value of cro.
     */
    public boolean getAddProbabilityColumns() {

        return addProbabilities;
    }

    /**
     * Set the value of cro.
     *
     * @param crossoverProbability Value to assign to cro.
     */
    public void setAddProbabilityColumns(boolean value) {

        addProbabilities = value;
        if (addProbabilities && ruleExtractionClassifier instanceof weka.classifiers.trees.Grex)
            ((weka.classifiers.trees.Grex) ruleExtractionClassifier).useProbabilities();
    }

    /**
     * Returns the tip text for this property
     * @return tip text for this property suitable for
     * displaying in the explorer/experimenter gui
     */
    public String baseClassifierTipText() {
        return "The base classifier to be used.";
    }

    /**
     * Sets the base classifer.
     *
     * @param classifier a base classifier with all options set.
     */
    public void setBaseClassifier(Classifier classifier) {

        baseClassifier = classifier;
    }

    /**
     * Gets the base classifer.
     *
     * @return the Base Classifier
     */
    public Classifier getBaseClassifier() {

        return baseClassifier;
    }

    /**
     * Returns the tip text for this property
     * @return tip text for this property suitable for
     * displaying in the explorer/experimenter gui
     */
    public String ruleExtractionClassifierTipText() {
        return "The rule extraction classifier to be used.";
    }

    /**
     * Sets the rule extraction classifer.
     *
     * @param classifier a rule extraction classifier with all options set.
     */
    public void setRuleExtractionClassifier(Classifier classifier) {

        ruleExtractionClassifier = classifier;
    }

    /**
     * Gets the rule extraction classifer.
     *
     * @return the Rule Extraction Classifier
     */
    public Classifier getRuleExtractionClassifier() {

        return ruleExtractionClassifier;
    }

    /**
     * Output a representation of this classifier
     */
    public String toString() {

        if (baseClassifier == null) {
            return "RuleExtract: No base model built yet.";
        }

        if (ruleExtractionClassifier == null) {
            return "RuleExtract: No rule extraction model built yet.";
        }

        String result = "Base classifier is:\n";
        result += baseClassifier.toString();
        result += "\nRule Extraction classifier is:\n";
        result += ruleExtractionClassifier.toString();

        return result;
    }

    public static void main(String[] args) throws Exception {
        runClassifier(new RuleExtract(), args);

    }

    public Enumeration enumerateMeasures() {
        if (ruleExtractionClassifier instanceof AdditionalMeasureProducer) {
            return ((AdditionalMeasureProducer) ruleExtractionClassifier).enumerateMeasures();
        }
        return (new Vector()).elements();
    }

    public double getMeasure(String measureName) {
        if (ruleExtractionClassifier instanceof AdditionalMeasureProducer) {
            return ((AdditionalMeasureProducer) ruleExtractionClassifier).getMeasure(measureName);
        }
        return 0;
    }

    public double classifyInstance(Instance instance) throws Exception {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}
